#region License, Terms and Conditions
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using facebook.Schema;
using facebook.Utility;
using Jayrock;
using pesta.Data;
using Pesta.DataAccess.Azure;
using Pesta.DataAccess.SqlServer;
using raya.Library;

namespace raya.Models.login
{
    public class login
    {
        public readonly Facebook fbclient = new Facebook();
        public readonly GoogleFriendConnect gfc = new GoogleFriendConnect();
        public string id { get; set; }
#if AZURE
        public long fbuid { get; set; }
#else
        public long? fbuid { get; set; }
#endif
        public string emailAddress { get; set; }
        public string name { get; set; }
        public string gfcUid { get; set; }
        public login()
        {
        }
        private login(person info)
            :this()
        {
            emailAddress = info.email;
            name = info.first_name + " " + info.last_name;
            id = info.id.ToString();
        }

        public void SetSession(HttpSessionStateBase session)
        {
            session["id"] = id;
            session["fbuid"] = fbuid;
            session["username"] = name;
            session["email"] = emailAddress;
        }
        public login GetLoggedIn()
        {
            var nativeUser = getAlreadyLoggedIn();
            login user = null;
            long fbUid = 0;
            
            if (IsConnectedToFacebook())
            {
                fbUid = fbclient.api.uid;
            }
            if (fbUid != 0)
            {
                user = GetByFacebookUid(fbUid);

                if (nativeUser != null)
                {
                    // connect their accounts.
                    nativeUser.ConnectWithFacebookUid(fbUid);
                    user = nativeUser;
                }
                else if (user == null)
                {
                    // create new local account
                    user = CreateFromFacebookUid(fbUid);
                }
            }
            else
            {
                // try obtain google friend connect id
                Person gfcUser = null;
                if (IsConnectedToGFC())
                {
                    gfcUser = gfc.GetViewerData();
                }

                if (gfcUser != null)
                {
                    user = GetByGFC(gfcUser.id);
                    if (nativeUser != null)
                    {
                        // connect their accounts.
                        nativeUser.ConnectWithGFC(gfcUser.id);
                        user = nativeUser;
                    }
                    else if (user == null)
                    {
                        // create new local account
                        user = CreateFromGFC(gfcUser);
                    }
                }
            }

            if (nativeUser != null && user == null)
            {
                user = nativeUser;
            }

            return user;
        }

        private void ConnectWithGFC(string gfcUid)
        {
            // if there is already a GFC account
            login user = GetByGFC(gfcUid);
            if (user != null)
            {
                if (user.id != id)
                {
                    user.Delete();
                }
            }

            this.gfcUid = gfcUid;
            people.people.save_gfc_person(id, gfcUid);

        }

        static login getAlreadyLoggedIn()
        {
            // check the logged-in userid first
            var ctx = HttpContext.Current;
            if (ctx.Session["id"] != null &&
                ctx.Session["fbuid"] != null &&
                ctx.Session["email"] != null &&
                ctx.Session["username"] != null)
            {
                var user = new login
                               {
                                   fbuid = (long)ctx.Session["fbuid"],
                                   id = ctx.Session["id"].ToString(),
                                   emailAddress = ctx.Session["email"].ToString(),
                                   name = ctx.Session["username"].ToString()
                               };
                return user;
            }
            /*
            if (ctx.Request.Cookies["authenticated"] != null)
            {
                // user is not logged in yet, but we do have a authenticated cookie, see if it's valid
                // and if so setup the session
                return login.get_authenticated(HttpUtility.UrlDecode(ctx.Request.Cookies["authenticated"].Value));
            }
            */
            return null;
        }

        bool IsFacebookUser()
        {
            return (fbuid > 0);
        }

        bool IsGFCUser()
        {
            return (!string.IsNullOrEmpty(gfcUid));
        }

        public bool IsConnectedToFacebook()
        {
            return fbclient.isConnected();
        }

        public bool IsConnectedToGFC()
        {
            return gfc.isConnected();
        }

        public void LogOut()
        {
            HttpContext.Current.Session.Clear();
            HttpContext.Current.Response.Cookies["authenticated"].Expires = DateTime.Now.AddMonths(-1);
            if (IsFacebookUser() && IsConnectedToFacebook())
            {
                fbclient.api.auth.expireSession();
                Facebook.ClearFacebookCookies();
            }
        }
        private bool ConnectWithFacebookUid(long fbUid)
        {
            // if there is already a facebook account
            login user = GetByFacebookUid(fbUid);
            if (user != null)
            {
                // if there are two separate accounts for the same
                // user, then delete the Facebook-specific one
                // and connect the facebook id with the existing
                // account.
                //
                // a real site wouldn't actually delete an account -
                // the user should probably control the merging
                // of data from the to-be-deleted account to their own
                if (user.id != id)
                {
                    user.Delete();
                }
            }

            if (fbUid == 0)
            {
                return false;
            }
            fbuid = fbUid;
#if AZURE
#else
            people.people.save_fb_person(id, fbUid);
#endif

            // since we have an email now, register with facebook
            if(!string.IsNullOrEmpty(emailAddress))
            {
                return Register(emailAddress, id);
            }
            return true;
        }
        public void DisconnectFromFacebook() 
        {
            if (!String.IsNullOrEmpty(emailAddress) && IsConnectedToFacebook()) 
            {
                Unregister(emailAddress);
            }
#if AZURE
            if (fbuid != 0)
            {
                people.people.remove_fb_person(fbuid);
                fbuid = 0;
            }
#else
            if (fbuid.HasValue)
            {
                people.people.remove_fb_person(fbuid.Value);
                fbuid = null;
            }
#endif
        }
        
        public string GetProfilePic(bool showLogo, Img.size size)
        {
            return GetProfilePic(showLogo, size, false);
        }
        public string GetProfilePic(bool showLogo, Img.size size, bool linked)
        {
            string thumb = RayaSettings.Site_root + "/images/people/" + id + ".jpg";
            bool nophoto = !File.Exists(AppDomain.CurrentDomain.BaseDirectory + thumb);
#if AZURE
            if (nophoto)
            {
                nophoto = !AzureBlob.DoesBlobItemExists(Img.blobImageContainerName, id + ".jpg");
            }
#endif
            
            var dim = Img.getImageDimensionsFromSize(size);
            
            // only get facebook photo if we don't have a local copy
            if (nophoto && IsFacebookUser())
            {
                if (linked)
                {
                    return "<fb:profile-pic linked=\"true\" uid=\"" + fbuid + "\" width=\"" + dim.First + "\" " +
                       (showLogo ? " facebook-logo=\"true\"" : "") + "></fb:profile-pic>";
                }
                return "<a href=\"" + RayaSettings.Web_prefix + "/profile/" + id + "\" rel=\"me\"><fb:profile-pic linked=\"" +
                        linked + "\" uid=\"" + fbuid + "\" width=\"" + dim.First + "\" " +
                       (showLogo ? " facebook-logo=\"true\"" : "") + "></fb:profile-pic></a>";
            }
            if (nophoto)
            {
                thumb = RayaSettings.Site_root + Utility.noPhotoThumbnailURL;
            }
            else
            {
#if AZURE
                thumb = Img.by_size(thumb, (int)dim.First, (int)dim.Second, false);
#else
                thumb = RayaSettings.Site_root + Img.by_size(thumb, (int)dim.First, (int)dim.Second, false);
#endif
            }

            return "<a href=\"" + RayaSettings.Web_prefix + "/profile/" + id + "\" rel=\"me\"><img src=\"" +
                        thumb + "\" alt=\"" + name + "\" title=\"" + name + "\" /></a>";
        }
        string GetStatus()
        {
            return (IsFacebookUser()) ?
                    "<fb:user-status uid=\"" + fbclient.api.uid + "\" ></fb:user-status>" : "";
        }
        bool Register(string userEmail, string userid)
        {
            var emailHash = Utility.email_get_public_hash(userEmail);
            var accountUrl = "http://" + HttpContext.Current.Request.Url.Authority + "/profile/" + userid;
            var accounts = new List<Dictionary<string, string>> { new Dictionary<string, string> { { "email_hash", emailHash }, { "account_url", accountUrl } } };
            bool ok = false;
            try
            {
                ok = fbclient.api.connect.registerUsers(accounts);
            }
            catch (FacebookException)
            {
                
            }
            return ok;
        }
        
        bool Unregister(string userEmail)
        {
            return fbclient.api.connect.unregisterUsers(new List<string> { Utility.email_get_public_hash(userEmail) });
        }

        public override bool Equals(object obj)
        {
            return ((login)obj).id == id;
        }
        public override int GetHashCode()
        {
            return id.GetHashCode();
        }

#if AZURE
        private void Delete()
        {
            var p = new people.people();
            p.RemovePerson(id);

            // Unregister the account from fb
            if (!string.IsNullOrEmpty(emailAddress))
            {
                Unregister(emailAddress);
            }
        }

        private void Save(PersonRow person)
        {
            using (var db = RayaDataContext.DB())
            {
                db.InsertOnSubmit(AzureRayaDataContext.TableNames.persons,person);
                db.SubmitChanges();
            }
        }
        
        private login GetByGFC(string s)
        {
            using (var db = RayaDataContext.DB())
            {
                var p = db.persons.Where(x => x.gfc_id == gfcUid).SingleOrDefault();
                if (p == null)
                {
                    return null;
                }
                return p.ToLogin();
            }
        }

        private static login GetByFacebookUid(long uid)
        {
            using (var db = RayaDataContext.DB())
            {
                var result = db.persons.Where(x => x.fb_userid == uid).SingleOrDefault();
                if (result == null)
                {
                    return null;
                }
                return result.ToLogin();
            }
        }

        public static login Authenticate(string email, string password)
        {
            using (var db = RayaDataContext.DB())
            {
                var result = db.persons.Where(p => p.email == email && 
                                                   p.password == password).SingleOrDefault();
                if (result == null)
                {
                    return null;
                }

                return result.ToLogin();
            }
        }

        login CreateFromFacebookUid(long fbUid)
        {
            var fbUserInfo = fbclient.api.users.getInfo(fbUid);

            PersonRow person = new PersonRow(DateTime.UtcNow.Ticks.ToString(),string.Empty)
            {
                first_name = fbUserInfo.first_name,
                last_name = fbUserInfo.last_name,
                // TODO: need to parse birthday properly
                //date_of_birth = fbUserInfo.birthday_date.ToUnixTime(),
                gender = fbUserInfo.sex,
                email = "",
                password = "",
                fb_userid = fbUid
            };
            Save(person);
            return person.ToLogin();
        }

        private login CreateFromGFC(Person user)
        {
            PersonRow person = new PersonRow(DateTime.UtcNow.Ticks.ToString(), string.Empty)
                                   {
                                       email = "",
                                       password = "",
                                       email_hash = "",
                                       thumbnail_url = user.thumbnailUrl,
                                       gfc_id = user.id
            };
            if (user.name != null)
            {
                person.first_name = user.name.givenName;
                person.last_name = user.name.familyName;
            }
            else
            {
                person.first_name = user.displayName;
                person.last_name = "";
            }
            Save(person);
            return person.ToLogin();
        }

        public static login GetUser(string uid)
        {
            using (var db = RayaDataContext.DB())
            {
                var result = db.persons.Where(x => x.PartitionKey == uid).SingleOrDefault();
                if (result == null)
                {
                    return null;
                }
                return result.ToLogin();
            }
        }

        public static login GetUserByEmail(string email)
        {
            using (var db = RayaDataContext.DB())
            {
                var result = db.persons.Where(x => x.email == email).SingleOrDefault();
                if (result == null)
                {
                    return null;
                }
                return result.ToLogin();
            }
        }

        public string registerPerson(string emailAdd, string passwd, string firstName, string lastName, Person.Gender gender, DateTime? dateOfBirth)
        {
            try
            {
                using (var db = RayaDataContext.DB())
                {
                    var existing = Enumerable.SingleOrDefault(db.persons, p => p.email == emailAdd);
                    if (existing != null)
                    {
                        throw new Exception("email address is already in use");
                    }

                    existing = new PersonRow(DateTime.UtcNow.Ticks.ToString(), string.Empty)
                    {
                        email = emailAdd,
                        email_hash = Utility.email_get_public_hash(emailAdd),
                        password = passwd,  // TODO should store hash not original password
                        first_name = firstName,
                        last_name = lastName,
                        gender = gender.ToString(),
                        date_of_birth = UnixTime.ToInt64(dateOfBirth.Value)
                    };
                    db.InsertOnSubmit(AzureRayaDataContext.TableNames.persons, existing);
                    db.SubmitChanges();

                    // Register user with facebook
                    Register(emailAdd, existing.id);
                    return existing.id;
                }
            }
            catch (Exception ex)
            {
                throw new Exception("fail to add new person: " + ex.Message);
            }
        }
#else
        login CreateFromFacebookUid(long fbUid)
        {
            var fbUserInfo = fbclient.api.users.getInfo(fbUid);

            // if we found a user, just add a uid; otherwise create a new one
            fb_user fbUser = new fb_user { fb_userid = fbUid };
            person person = new person
                                {
                                    first_name = fbUserInfo.first_name,
                                    last_name = fbUserInfo.last_name,
                                    // TODO: need to parse birthday properly
                                    //date_of_birth = fbUserInfo.birthday_date.ToUnixTime(),
                                    gender = fbUserInfo.sex,
                                    email = "",
                                    password = ""
                                };
            Save(person, fbUser);
            return new login(person) { fbuid = fbUid };
        }

        private login CreateFromGFC(Person user)
        {
            gfc_user gfcUser = new gfc_user { gfc_id = user.id };
            person person = new person
                                {
                                    email="", 
                                    password="",
                                    email_hash="",
                                    thumbnail_url = user.thumbnailUrl
                                };
            if (user.name != null)
            {
                person.first_name = user.name.givenName;
                person.last_name = user.name.familyName;
            }
            else
            {
                person.first_name = user.displayName;
                person.last_name = "";
            }
            Save(person, gfcUser);
            return new login(person) { gfcUid = user.id };
        }

        public static login Authenticate(string email, string password)
        {
            using (var db = RayaDataContext.DB())
            {
                var res = (from p in db.persons
                           join fbusers in db.fb_users on p.id equals fbusers.personid into tempfbusers
                           from f in tempfbusers.DefaultIfEmpty()
                           where p.email == email && p.password == password
                           select new login
                           {
                               id = p.id.ToString(),
                               name = p.first_name + " " + p.last_name,
                               emailAddress = p.email,
                               fbuid = f != null ? f.fb_userid : 0
                           }).SingleOrDefault();
                return res;
            }
        }

        private login GetByGFC(string gfcUid)
        {
            using (var db = RayaDataContext.DB())
            {
                return (from p in db.persons
                        where p.gfc_user.gfc_id == gfcUid
                        select new login
                        {
                            id = p.id.ToString(),
                            name = p.first_name + " " + p.last_name,
                            emailAddress = p.email,
                            gfcUid = gfcUid
                        }).SingleOrDefault();
            }
        }

        public static login GetUser(int uid)
        {
            using (var db = RayaDataContext.DB())
            {
                return (from a in db.persons
                        join fbusers in db.fb_users on a.id equals fbusers.personid into tempfbusers
                        from f in tempfbusers.DefaultIfEmpty()
                        where a.id == uid
                        select new login
                        {
                            id = a.id.ToString(),
                            name = a.first_name + " " + a.last_name,
                            emailAddress = a.email,
                            fbuid = f!= null? f.fb_userid:0
                        }).SingleOrDefault();
            }
        }

        public static login GetUserByEmail(string email)
        {
            using (var db = RayaDataContext.DB())
            {
                return (from a in db.persons
                        join fbusers in db.fb_users on a.id equals fbusers.personid into tempfbusers
                        from f in tempfbusers.DefaultIfEmpty()
                        where a.email == email
                        select new login
                        {
                            id = a.id.ToString(),
                            name = a.first_name + " " + a.last_name,
                            emailAddress = a.email,
                            fbuid = f != null ? f.fb_userid : 0
                        }).SingleOrDefault();
            }
        }

        string GetName()
        {
            // override the user's name if it is a facebook user, regardless
            if (IsFacebookUser())
            {
                var _info = fbclient.api.users.getInfo(fbuid.Value);
                if (_info != null)
                {
                    return _info.name;
                }
            }
            throw new NotImplementedException();
            //return first_name + " " + last_name;
        }

        string GetEmail()
        {
            // user can override themselves if they want to
            return (IsFacebookUser() && string.IsNullOrEmpty(emailAddress)) ?
                                "" : emailAddress;
        }

        string GetFacebookBadge()
        {
            return (IsFacebookUser()) ?
                    "<img src=\"http://static.ak.fbcdn.net/images/icons/favicon.gif\" />" : "&nbsp;";
        }

        private static login GetByFacebookUid(long fbUid)
        {
            using (var db = RayaDataContext.DB())
            {
                return (from p in db.persons
                        where p.fb_user.fb_userid == fbUid
                        select new login
                        {
                            id = p.id.ToString(),
                            name = p.first_name + " " + p.last_name,
                            emailAddress = p.email,
                            fbuid = fbUid
                        }).SingleOrDefault();
            }
        }

        person GetByFacebookEmailHashes(user user)
        {
            if (user == null || 
                user.email_hashes == null || 
                user.email_hashes.email_hashes_elt.Count == 0)
            {
                return null;
            }

            using (var db = RayaDataContext.DB())
            {
                var ret = db.persons.Where(x => user.email_hashes.email_hashes_elt.AsEnumerable().Contains(x.email_hash)).Select(x => x).SingleOrDefault();
                // NOTE: if a user has multiple emails on their facebook account,
                // and more than one is registered on the site, then we will
                // only return the first one.
                if (ret != null)
                {
                    // Since we we looked up by email_hash, save the fb_uid
                    // so we can look up directly next time
                    emailAddress = ret.email;
                    fbuid = user.uid.Value;
                    //first_name = ret.first_name;
                    //last_name = ret.last_name;
                    id = ret.id.ToString();
                    SaveFbuid();
                    return ret;
                }
            }
            return null;
        }

        public int registerPerson(string emailAdd, string passwd, string firstName, string lastName, Person.Gender gender, DateTime? dateOfBirth)
        {
            try
            {
                using (var db = RayaDataContext.DB())
                {
                    person existing = Enumerable.SingleOrDefault(db.persons, p => p.email == emailAdd);
                    if (existing != null)
                    {
                        throw new Exception("email address is already in use");
                    }

                    existing = new person
                    {
                        email = emailAdd,
                        email_hash = Utility.email_get_public_hash(emailAdd),
                        password = passwd,  // TODO should store hash not original password
                        first_name = firstName,
                        last_name = lastName,
                        gender = gender.ToString(),
                        date_of_birth = UnixTime.ToInt64(dateOfBirth.Value)
                    };
                    db.persons.InsertOnSubmit(existing);
                    db.SubmitChanges();

                    // Register user with facebook
                    Register(emailAdd, existing.id.ToString());
                    return existing.id;
                }
            }
            catch (Exception ex)
            {
                throw new Exception("fail to add new person: " + ex.Message);
            }

        }

        private void SaveFbuid()
        {
            if (!fbuid.HasValue)
            {
                return;
            }
            int userid = int.Parse(id);
            using (var db = RayaDataContext.DB())
            {
                var existing = db.fb_users.SingleOrDefault(x => x.personid == userid);
                if (existing == null)
                {
                    existing = new fb_user() { personid = userid };
                    db.fb_users.InsertOnSubmit(existing);
                }
                existing.fb_userid = fbuid.Value;
                db.SubmitChanges();
            }
        }

        private void Save(person person, fb_user fbUser)
        {
            using (var db = RayaDataContext.DB())
            {
                // update persons table
                person.email_hash = !string.IsNullOrEmpty(person.email) 
                        ? Utility.email_get_public_hash(person.email) : "";
                db.persons.InsertOnSubmit(person);
                db.SubmitChanges();
                id = person.id.ToString();
                // update fb users table
                fbUser.personid = person.id;
                db.fb_users.InsertOnSubmit(fbUser);
                db.SubmitChanges();
            }
        }
        private void Save(person person, gfc_user gfcUser)
        {
            using (var db = RayaDataContext.DB())
            {
                // update persons table
                db.persons.InsertOnSubmit(person);
                db.SubmitChanges();
                id = person.id.ToString();
                // update fb users table
                gfcUser.personid = person.id;
                db.gfc_users.InsertOnSubmit(gfcUser);
                db.SubmitChanges();
            }
        }

        private void SaveAndRegister(person person, fb_user fbUser)
        {
            Save(person, fbUser);

            // If that was successful, register the user with Facebook
            Register(person.email, person.id.ToString());
        }

        private bool Delete()
        {
            // TODO: log this of duplicate account 
            var p = new people.people();
            p.RemovePerson(int.Parse(id));

            // Unregister the account from fb
            if (!string.IsNullOrEmpty(emailAddress))
            {
                return Unregister(emailAddress);
            }
            return true;
        }

        public void RemoveFbuid()
        {
            if (!fbuid.HasValue)
            {
                return;
            }
            people.people.remove_fb_person(fbuid.Value);
        }
#endif


    }
}